﻿using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.VCCodeModel;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;

namespace CodeJumper
{
    public class HookInformation
    {
        public DTE Dte;
        public DTE2 Dte2;

        public DocumentEvents DocumentEvents;
        public CodeModelEvents CodeModelEvents;

        public long totalInstances;
        public long currentInstances;
        public long errorCount;

        public HookInformation()
        {
            Dte = Package.GetGlobalService(typeof(DTE)) as DTE;
            Dte2 = Dte as DTE2;

            try
            {
                if (Dte2.Solution.Projects.Count > 0)
                {
                    // Don't remove the dynamic x & Projects.Count, this actually initializes the CodeDOM.
                    DTE2 dte2 = Dte2;
                    dynamic x = dte2;
                    int count = dte2.Solution.Projects.Count;
                }
            }
            catch (COMException)
            {
            }

            DocumentEvents = (Dte.Events as Events2).get_DocumentEvents(null);
            CodeModelEvents = (Dte.Events as Events2).get_CodeModelEvents(null);
        }

    }

    class InstanceManager
    {
        public HookInformation HookInformation = new HookInformation();
        Dictionary<int, ApplicationConfiguration> instances = new Dictionary<int,ApplicationConfiguration>();

        public static InstanceManager Instance = new InstanceManager();

        public Random Random = new Random();

        private InstanceManager()
        {
        }

        ///-------------------------------------------------------------------------------------------------
        /// <summary>
        ///     Instance hint. This is to initially hint the Instance Manager that the first ever instance was created.
        ///     We could create a more global add-in, but due to lazyness this should also do for now.
        /// </summary>
        ///
        /// <param name="appConfig">    The application configuration. </param>
        ///-------------------------------------------------------------------------------------------------
        public void InstanceHint(ApplicationConfiguration appConfig)
        {
            lock (instances)
            {
                if (HookInformation.totalInstances == 0)
                {
                    SetupEvents(appConfig);
                }

                AddInstance(appConfig);
            }
        }

        public void AddInstance(ApplicationConfiguration appConfig)
        {
            lock (instances)
            {
                if(instances.Keys.Contains(appConfig.ApplicationId))
                {
                    ++HookInformation.errorCount;
                    Trace.WriteLine(string.Format("Error Id {0} is already present in IM", appConfig.ApplicationId));
                    return;
                }


                ++HookInformation.currentInstances;
                ++HookInformation.totalInstances;
                instances[appConfig.ApplicationId] = appConfig;
            }

            PrintStatus();
        }

        public void RemoveInstance(ApplicationConfiguration appConfig)
        {
            lock(instances)
            {
                if (!instances.Keys.Contains(appConfig.ApplicationId))
                {
                    ++HookInformation.errorCount;
                    Trace.WriteLine(string.Format("Error Id {0} is not present in IM", appConfig.ApplicationId));
                    return;
                }
                --HookInformation.currentInstances;
                instances.Remove(appConfig.ApplicationId);
            }

            PrintStatus();
        }

        public void RemoveInstance(Document document)
        {
            var ac = FindInstance(document);

            if (ac == null)
            {
                Trace.WriteLine(string.Format("Error, no instance found to document {0}", document.FullName));
                return;
            }

            RemoveInstance(ac);
        }


        public ApplicationConfiguration FindInstance(Document document)
        {
            lock (instances)
            {
                foreach (var kvp in instances)
                {
                    if (kvp.Value.DocumentFilename == document.FullName)
                        return kvp.Value;
                }
            }
            return null;
        }


        public ApplicationConfiguration FindInstance(CodeElement element)
        {
            try
            {
                return FindInstance(element.ProjectItem.Document);
            }
            catch (Exception e)
            {
                Trace.WriteLine(string.Format("IM FindInstance Exception {0}", e));
            }
            return null;
        }

        public void PrintStatus()
        {
            lock (instances)
            {
                Trace.WriteLine(string.Format("Current number of instances: {0}", instances.Count));

                foreach (var inst in instances.Values)
                {
                    Trace.WriteLine(string.Format("Id: {0}, Document: {1}", inst.ApplicationId, inst.DocumentFilename));
                }
            }
        }

        private void SetupEvents(ApplicationConfiguration appConfig)
        {
            HookInformation.CodeModelEvents.ElementAdded += CodeModelEvents_ElementAdded;
            HookInformation.CodeModelEvents.ElementChanged += CodeModelEvents_ElementChanged;
            HookInformation.CodeModelEvents.ElementDeleted += CodeModelEvents_ElementDeleted;

            HookInformation.DocumentEvents.DocumentOpened += DocumentEvents_DocumentOpened;
            HookInformation.DocumentEvents.DocumentClosing += DocumentEvents_DocumentClosing;
        }



        void CodeModelEvents_ElementAdded(CodeElement Element)
        {
            Trace.WriteLine(string.Format("IM Element added {0}", Element.Name));

            
            // There are still problems getting the unique ID for C code elements. It would require to step of the parents of that element and create a fully classified FullName.
            // Since these events are called twice. Once for the parent (Change event) and once for the element removed itself, We can simply ignore it for now.
            if (Element.Language == EnvDTECodeModelLanguageConstants.vsCMLanguageMC || Element.Language == EnvDTECodeModelLanguageConstants.vsCMLanguageVC)
            {
                Trace.WriteLine("Aborting delete operation for C/C++ code.");
                return;
            }



            var app = FindInstance(Element);

            app.InvokeCompleteUpdate();

        }

        void CodeModelEvents_ElementChanged(CodeElement Element, vsCMChangeKind Change)
        {
            CodeElement2 ce2 = (CodeElement2)Element;
            //Trace.WriteLine(string.Format("IM Element changed {0}, Kind {1}, Id {2}", Element.Name, Change, ce2.ElementID));

            var app = FindInstance(Element);


            switch (Change)
            {
                case vsCMChangeKind.vsCMChangeKindRename:
                    // TODO: Change Element only
                    
                    var renamedElement = app.DocumentParser.RootElement.TraverseChildrenForFirstThrow(app.DocumentParser.RootElement);
                    var parent = CodeElementHelper.GetParent(Element);
                    if (parent == null)
                    {
                        app.InvokeCompleteUpdate();
                    }
                    else
                        app.InvokeChangeUpdate(parent);

                    break;
                case vsCMChangeKind.vsCMChangeKindArgumentChange:
                    // TODO: Change element only
                    break;

                    // etc.
                default:
                    app.InvokeChangeUpdate(Element);
                    break;
            }

        }

        void CodeModelEvents_ElementDeleted(object Parent, CodeElement Element)
        {
           Trace.WriteLine(string.Format("IM Element deleted {0}", Element.Name));
            CodeElement parent = (CodeElement)Parent;

            // There are still problems getting the unique ID for C code elements. It would require to step of the parents of that element and create a fully classified FullName.
            // Since these events are called twice. Once for the parent (Change event) and once for the element removed itself, We can simply ignore it for now.
            if (parent.Language == EnvDTECodeModelLanguageConstants.vsCMLanguageMC || parent.Language == EnvDTECodeModelLanguageConstants.vsCMLanguageVC)
            {
                Trace.WriteLine("Aborting delete operation for C/C++ code.");
                return;
            }


            var app = FindInstance(Element);

            app.InvokeDeletionUpdate(Element);
        }

        void DocumentEvents_DocumentOpened(Document Document)
        {
            Trace.WriteLine(string.Format("IM Document opened {0}", Document.FullName));
        }

        void DocumentEvents_DocumentClosing(Document Document)
        {
            Trace.WriteLine(string.Format("IM Document closed {0}", Document.FullName));
            RemoveInstance(Document);
        }


    }
}
